/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import Long from '../util/long/index'
import Constants from './Constants'
import HuffmanCompressionTable from './HuffmanCompressionTable'
import BitOutputStream from './BitOutputStream'
import Huffman from './Huffman'

export default class HuffmanCompressor {
    constructor() {
    }

    public static compress4streams(outputBase: any, outputAddress: Long, outputSize: number, inputBase: Object, inputAddress: Long, inputSize: number, table: HuffmanCompressionTable): number
    {
        let input: Long = inputAddress;
        let inputLimit: Long = inputAddress.add(inputSize);
        let output: Long = outputAddress;
        let outputLimit: Long = outputAddress.add(outputSize);

        let segmentSize: number = (inputSize + 3) / 4;

        if (outputSize < 6 /* jump table */
        + 1 /* first stream */
        + 1 /* second stream */
        + 1 /* third stream */
        + 8 /* 8 bytes minimum needed by the bitstream encoder */
        ) {
            return 0; // minimum space to compress successfully
        }

        if (inputSize <= 6 + 1 + 1 + 1) { // jump table + one byte per stream
            return 0; // no saving possible: input too small
        }

        output = output.add(Constants.SIZE_OF_SHORT + Constants.SIZE_OF_SHORT + Constants.SIZE_OF_SHORT); // jump table

        let compressedSize: number;

        compressedSize = this.compressSingleStream(outputBase, output,
        outputLimit.subtract(output).toInt(), inputBase, input, segmentSize, table);
        if (compressedSize == 0) {
            return 0;
        }
        Unsafe.putShort(outputBase, outputAddress.toNumber(), compressedSize);
        output = output.add(compressedSize);
        input = input.add(segmentSize);

        compressedSize = this.compressSingleStream(outputBase, output,
        outputLimit.subtract(output).toNumber(), inputBase, input, segmentSize, table);
        if (compressedSize == 0) {
            return 0;
        }
        Unsafe.putShort(outputBase, outputAddress.add(Constants.SIZE_OF_SHORT).toNumber(), compressedSize);
        output = output.add(compressedSize);
        input = input.add(segmentSize);

        compressedSize = this.compressSingleStream(outputBase, output,
        outputLimit.subtract(output).toNumber(), inputBase, input, segmentSize, table);
        if (compressedSize == 0) {
            return 0;
        }
        Unsafe.putShort(outputBase,
        outputAddress.add(Constants.SIZE_OF_SHORT + Constants.SIZE_OF_SHORT).toNumber(), compressedSize);
        output = output.add(compressedSize);
        input = input.add(segmentSize);

        compressedSize = this.compressSingleStream(outputBase, output,
        outputLimit.subtract(output).toNumber(), inputBase, input, inputLimit.subtract(input).toNumber(), table);
        if (compressedSize == 0) {
            return 0;
        }
        output = output.add(compressedSize);

        return output.subtract(outputAddress).toInt();
    }

    public static compressSingleStream(outputBase: any, outputAddress: Long, outputSize: number, inputBase: any, inputAddress: Long, inputSize: number, table: HuffmanCompressionTable): number
    {
        if (outputSize < Constants.SIZE_OF_LONG) {
            return 0;
        }

        let bitstream: BitOutputStream = new BitOutputStream(outputBase, outputAddress, outputSize);
        let input: Long = inputAddress;

        let n: number = inputSize & -4; // join to mod 4

        switch (inputSize & 3) {
            case 3:
                table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n + 2).toNumber()) & 0xFF);
                if (Constants.SIZE_OF_LONG * 8 < Huffman.MAX_TABLE_LOG * 4 + 7) {
                    bitstream.flush();
                }
            case 2:
                table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n + 1).toNumber()) & 0xFF);
                if (Constants.SIZE_OF_LONG * 8 < Huffman.MAX_TABLE_LOG * 2 + 7) {
                    bitstream.flush();
                }
            case 1:
                table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n + 0).toNumber()) & 0xFF);
                bitstream.flush();
            case 0: /* fall-through */
            default:
                break;
        }

        for (; n > 0; n -= 4) { // note: n & 3 == 0 at this stage
            table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n).sub(1).toNumber()) & 0xFF);
            if (Constants.SIZE_OF_LONG * 8 < Huffman.MAX_TABLE_LOG * 2 + 7) {
                bitstream.flush();
            }
            table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n).sub(2).toNumber()) & 0xFF);
            if (Constants.SIZE_OF_LONG * 8 < Huffman.MAX_TABLE_LOG * 4 + 7) {
                bitstream.flush();
            }
            table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n).sub(3).toNumber()) & 0xFF);
            if (Constants.SIZE_OF_LONG * 8 < Huffman.MAX_TABLE_LOG * 2 + 7) {
                bitstream.flush();
            }
            table.encodeSymbol(bitstream, Unsafe.getByte(inputBase, input.add(n).sub(4).toNumber()) & 0xFF);
            bitstream.flush();
        }

        return bitstream.close();
    }
}
